package com.phonemap.helper;

import static com.google.appengine.api.utils.SystemProperty.environment;
import static com.google.appengine.api.utils.SystemProperty.Environment.Value.Development;

import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.GeoPt;
import com.google.appengine.api.search.Document;
import com.google.appengine.api.search.Field;
import com.google.appengine.api.search.GeoPoint;
import com.google.appengine.api.search.Index;
import com.google.appengine.api.search.IndexSpec;
import com.google.appengine.api.search.Query;
import com.google.appengine.api.search.QueryOptions;
import com.google.appengine.api.search.Results;
import com.google.appengine.api.search.ScoredDocument;
import com.google.appengine.api.search.SearchServiceFactory;
import com.google.appengine.api.search.SortExpression;
import com.google.appengine.api.search.SortOptions;
import com.phonemap.common.AppConstant;
import com.phonemap.dto.UserInfo;
import com.phonemap.models.User;
import com.phonemap.search.SearchIndexManager;

/**
 * The Class UserHelper.
 */
public final class UserHelper {

	/** The Constant log. */
	private static Logger logger = Logger.getLogger(UserHelper.class.getName());

	/**
	 * Gets the index.
	 * 
	 * @return the index
	 */
	public static Index getIndex() {
		IndexSpec indexSpec = IndexSpec.newBuilder()
				.setName(AppConstant.ENTITY_USER).build();
		return SearchServiceFactory.getSearchService().getIndex(indexSpec);
	}

	/**
	 * Instantiates a new user helper.
	 */
	private UserHelper() {

	}

	/**
	 * Builds the document.
	 * 
	 * @param userId
	 *            the user id
	 * @param fullname
	 *            the fullname
	 * @param gender
	 *            the gender
	 * @param located
	 *            the located
	 * @return the document
	 */
	public static Document buildDocument(final long userId,
			final String fullname, final String gender, final GeoPt located) {

		logger.info("Build User Index");
		GeoPoint geoPoint = new GeoPoint(located.getLatitude(),
				located.getLongitude());
		Document.Builder builder = Document
				.newBuilder()
				.addField(
						Field.newBuilder().setName(User.USER_ID_FIELD)
								.setText(Long.toString(userId)))
				.addField(
						Field.newBuilder().setName(User.FULLNAME_FIELD)
								.setText(fullname))
				.addField(
						Field.newBuilder().setName(User.GENDER_FIELD)
								.setText(gender))
				.addField(
						Field.newBuilder().setName(User.LOCATED_FIELD)
								.setGeoPoint(geoPoint));

		// geo-location doesn't work under dev_server, so let's add another
		// field to use for retrieving documents
		if (environment.value() == Development) {
			builder.addField(Field.newBuilder().setName("value").setNumber(1));
		}

		Document place = builder.build();

		return place;
	}

	/**
	 * Gets the users.
	 * 
	 * @param location
	 *            the location
	 * @param distanceInMeters
	 *            the distance in meters
	 * @param offset
	 *            the offset
	 * @return the users
	 */
	public static List<Entity> getUsers(final GeoPoint location,
			final int distanceInMeters, final int offset) {

		// TODO(user): Use memcache

		String geoPoint = "geopoint(" + location.getLatitude() + ", "
				+ location.getLongitude() + ")";

		String query = "distance(located, " + geoPoint + ") < "
				+ distanceInMeters; // + " and " + "distance(located, " +
									// geoPoint + ") > 0";
		String locExpr = "distance(located, " + geoPoint + ")";

		SortExpression sortExpr = SortExpression.newBuilder()
				.setExpression(locExpr)
				.setDirection(SortExpression.SortDirection.ASCENDING)
				.setDefaultValueNumeric(distanceInMeters + 1).build();
		Query searchQuery = Query
				.newBuilder()
				.setOptions(
						QueryOptions
								.newBuilder()
								.setSortOptions(
										SortOptions.newBuilder()
												.addSortExpression(sortExpr))
								.setOffset(offset * AppConstant.MAX_PAGING)
								.setLimit(AppConstant.MAX_PAGING)).build(query);
		Results<ScoredDocument> results = getIndex().search(searchQuery);

		if (results.getNumberFound() == 0) {
			// geo-location doesn't work under dev_server
			if (environment.value() == Development) {
				// return all documents
				searchQuery = Query
						.newBuilder()
						.setOptions(
								QueryOptions
										.newBuilder()
										.setSortOptions(
												SortOptions.newBuilder()
														.addSortExpression(
																sortExpr))
										.setOffset(
												offset * AppConstant.MAX_PAGING)
										.setLimit(AppConstant.MAX_PAGING))
						.build("value > 0");
				results = getIndex().search(searchQuery);
			}
		}

		List<Entity> users = new ArrayList<Entity>();
		Entity user = null;

		for (ScoredDocument document : results) {
			user = User.getUser(Long.parseLong(document.getOnlyField(
					User.USER_ID_FIELD).getText()));
			users.add(user);
		}

		return users;

	}

	/**
	 * Gets the user infos.
	 * 
	 * @param userId
	 *            the user id
	 * @param fullname
	 *            the fullname
	 * @param resultCount
	 *            the result count
	 * @return the user infos
	 */
	public static List<UserInfo> getUserInfos(final long userId,
			final String fullname, final int resultCount) {

		Results<ScoredDocument> results = getIndex().search(fullname);

		List<UserInfo> users = new ArrayList<UserInfo>();

		int index = 0;
		for (ScoredDocument document : results) {
			if (index >= resultCount) {
				break;
			}

			GeoPoint p = document.getOnlyField(User.LOCALE_FIELD).getGeoPoint();

			UserInfo userInfo = new UserInfo();
			userInfo.setUserId(Long.parseLong(document.getOnlyField(
					User.USER_ID_FIELD).getText()));
			userInfo.setFullname(document.getOnlyField(User.FULLNAME_FIELD)
					.getText());

			userInfo.setLocated(new GeoPt((float) p.getLatitude(), (float) p
					.getLongitude()));

			users.add(userInfo);
			index++;
		}

		return users;
	}

	/**
	 * Gets the users.
	 * 
	 * @param userId
	 *            the user id
	 * @param fullname
	 *            the fullname
	 * @param offset
	 *            the offset
	 * @return the users
	 */
	public static List<Entity> getUsers(final String userId,
			final String fullname, final int offset) {

		// Build the SortOptions with 2 sort keys
		SortOptions sortOptions = SortOptions
				.newBuilder()
				.addSortExpression(
						SortExpression
								.newBuilder()
								.setExpression(User.FULLNAME_FIELD)
								.setDirection(
										SortExpression.SortDirection.DESCENDING)
								.setDefaultValue(AppConstant.EMPTY_STRING))
				.setLimit(AppConstant.LIMIT_NUM).build();

		// Build the QueryOptions
		QueryOptions options = QueryOptions.newBuilder()
				.setOffset(offset * AppConstant.MAX_PAGING)
				.setLimit(AppConstant.MAX_PAGING)
				.setFieldsToReturn("user_id", "fullname", "located")
				.setSortOptions(sortOptions).build();

		// A query string
		String queryString = User.FULLNAME_FIELD + AppConstant.COLON + fullname;

		// Build the Query and run the search
		Query query = Query.newBuilder().setOptions(options).build(queryString);

		Results<ScoredDocument> results = getIndex().search(query);

		List<Entity> users = new ArrayList<Entity>();
		Entity user = null;

		for (ScoredDocument document : results) {
			user = User.getUser(Long.parseLong(document.getOnlyField(
					User.USER_ID_FIELD).getText()));
			users.add(user);
		}

		return users;
	}

	/**
	 * Gets the users.
	 * 
	 * @param fullname
	 *            the fullname
	 * @param offset
	 *            the offset
	 * @return the users
	 */
	public static List<Entity> getUsers(final String fullname, final int offset) {

		// Build the SortOptions with 2 sort keys
		SortOptions sortOptions = SortOptions
				.newBuilder()
				.addSortExpression(
						SortExpression
								.newBuilder()
								.setExpression(User.FULLNAME_FIELD)
								.setDirection(
										SortExpression.SortDirection.DESCENDING)
								.setDefaultValue(AppConstant.EMPTY_STRING))
				.setLimit(AppConstant.LIMIT_NUM).build();

		// Build the QueryOptions
		QueryOptions options = QueryOptions
				.newBuilder()
				.setOffset(offset * AppConstant.MAX_PAGING)
				.setLimit(AppConstant.MAX_PAGING)
				.setFieldsToReturn(User.USER_ID_FIELD, User.FULLNAME_FIELD,
						User.LOCATED_FIELD).setSortOptions(sortOptions).build();

		// A query string
		String queryString = User.FULLNAME_FIELD + AppConstant.COLON + fullname;

		// Build the Query and run the search
		Query query = Query.newBuilder().setOptions(options).build(queryString);

		Results<ScoredDocument> results = getIndex().search(query);

		List<Entity> users = new ArrayList<Entity>();
		Entity user = null;

		for (ScoredDocument document : results) {
			user = User.getUser(Long.parseLong(document.getOnlyField(
					User.USER_ID_FIELD).getText()));
			users.add(user);
		}

		return users;
	}

	/**
	 * Gets the distance in km.
	 * 
	 * @param latitude1
	 *            the latitude1
	 * @param longitude1
	 *            the longitude1
	 * @param latitude2
	 *            the latitude2
	 * @param longitude2
	 *            the longitude2
	 * @return the distance in km
	 */
	public static double getDistanceInKm(final double latitude1,
			final double longitude1, final double latitude2,
			final double longitude2) {
		final double earthRadius = 6378.1; // kilometers

		double lat1 = Math.toRadians(latitude1);
		double lat2 = Math.toRadians(latitude2);
		double long1 = Math.toRadians(longitude1);
		double long2 = Math.toRadians(longitude2);

		double dist = earthRadius
				* Math.acos(Math.sin(lat1) * Math.sin(lat2) + Math.cos(lat1)
						* Math.cos(lat2) * Math.cos(Math.abs(long1 - long2)));

		return dist;
	}

	/**
	 * Delete old index.
	 * 
	 * @param userId
	 *            the user id
	 */
	public static void deleteOldIndex(final long userId) {
		Index index = getIndex();
		Results<ScoredDocument> documents = SearchIndexManager.INSTANCE
				.retrieveDocuments(index, Long.toString(userId));
		for (ScoredDocument scoredDocument : documents) {
			// SearchIndexManager.INSTANCE.deleteDocumentFromIndex(scoredDocument
			// .getId());
			SearchIndexManager.INSTANCE.deleteDocumentFromIndex(index,
					scoredDocument.getId());
		}
	}
}
